/*
 * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.alg.filter.convolve.noborder;

import boofcv.misc.AutoTypeImage;
import boofcv.misc.CodeGeneratorBase;

import java.io.FileNotFoundException;

/**
 * @author Peter Abeles
 */
public class GenerateConvolvedUnrolled extends CodeGeneratorBase {

	final static int numUnrolled = 5;

	String typeKernel;
	String typeInput;
	String typeOutput;
	String dataKernel;
	String dataInput;
	String dataOutput;
	String bitWise;
	String sumType;
	boolean hasDivisor;
	boolean isInteger;

	String declareHalf;
	String divide;

	@Override
	public void generate() throws FileNotFoundException {
		create(AutoTypeImage.F32,AutoTypeImage.F32,false);
		create(AutoTypeImage.U8,AutoTypeImage.I8,true);
		create(AutoTypeImage.U8,AutoTypeImage.I16,false);
		create(AutoTypeImage.S16,AutoTypeImage.I16,false);
		create(AutoTypeImage.S16,AutoTypeImage.I16,true);
		create(AutoTypeImage.S32,AutoTypeImage.S32,false);
		create(AutoTypeImage.S32,AutoTypeImage.S32,true);
	}

	protected void create( AutoTypeImage inputImg , AutoTypeImage outputImg , boolean divided ) throws FileNotFoundException {
		isInteger = inputImg.isInteger();

		String name = "ConvolveImageUnrolled_"+inputImg.getAbbreviatedType()+"_"+outputImg.getAbbreviatedType();
		if( divided )
			name += "_Div";

		typeKernel = isInteger ? "I32" : "F32";
		typeInput = inputImg.getSingleBandName();
		typeOutput = outputImg.getSingleBandName();
		dataKernel = isInteger ? "int" : "float";
		dataInput = inputImg.getDataType();
		dataOutput = outputImg.getDataType();
		sumType = isInteger ? "int" : "float";
		bitWise = inputImg.getBitWise();
		hasDivisor = divided;

		declareHalf = isInteger ? "\t\tfinal " + sumType + " halfDivisor = divisor/2;\n" : "";
		divide = isInteger ? "(total+halfDivisor)/divisor" : "total/divisor";

		createFile(name);
	}

	public void createFile( String fileName ) throws FileNotFoundException {
		setOutputFile(fileName);
		printPreamble();
		createMaster("horizontal",1,hasDivisor);
		createMaster("vertical",1,hasDivisor);
		createMaster("convolve",2,hasDivisor);

		for (int i = 0; i < numUnrolled; i++) {
			addHorizontal(3 + i * 2,hasDivisor);
		}
		for (int i = 0; i < numUnrolled; i++) {
			addVertical(3 + i * 2, hasDivisor);
		}
		for (int i = 0; i < numUnrolled; i++) {
			if( hasDivisor )
				addConvolveDiv(3 + i * 2 );
			else
				addConvolve(3 + i * 2 );
		}

		out.println("}");
	}

	public void printPreamble() {
		out.print("import boofcv.struct.convolve.Kernel1D_" + typeKernel + ";\n");
		out.print("import boofcv.struct.convolve.Kernel2D_" + typeKernel + ";\n");
		out.print("import boofcv.struct.image." + typeInput + ";\n");
		if (typeInput.compareTo(typeOutput) != 0)
			out.print("import boofcv.struct.image." + typeOutput + ";\n");
		out.print("\n" +
				"/**\n" +
				" * <p>\n" +
				" * Unrolls the convolution kernel to reduce array accessing and save often used variables to the stack.\n" +
				" * </p>\n" +
				" *\n" +
				" * <p>\n" +
				" * Unrolling the image being convolved resulting in an additional 10% performance boost on a Core i7 processor,\n" +
				" * see commented out code below.  Due to the added complexity it was decided that this performance boost was\n" +
				" * not worth it.  By comparison, unrolling the kernel causes a performance boost between 2 and 3 times.\n" +
				" * </p>\n" +
				" * \n" +
				" * <p>\n" +
				" * DO NOT MODIFY.  This class was automatically generated by {@link GenerateConvolvedUnrolled}.\n" +
				" * </p>\n" +
				" *\n" +
				" * @author Peter Abeles\n" +
				" */\n" +
				"public class " + className + " {\n");
	}

	public void createMaster(String opName, int kernelDOF , boolean hasDivisor ) {
		String kernel = "Kernel"+kernelDOF+"D_"+typeKernel;

		out.print("\tpublic static boolean " + opName + "( " + kernel + " kernel ,\n" +
				"\t\t\t\t\t\t\t\t   " + typeInput + " image, " + typeOutput + " dest");


		if( hasDivisor ) {
			out.print(", int divisor ) {\n");
		} else {
			out.print(") {\n");
		}

		out.print(
				"\n" +
				"\t\t// Unrolled functions only exist for symmetric kernels with an odd width\n" +
				"\t\tif( kernel.offset != kernel.width/2 || kernel.width%2 == 0 )\n" +
				"\t\t\treturn false;\n" +
				"\n");

		out.print("\t\tswitch( kernel.width ) {\n");
		for (int i = 0; i < numUnrolled; i++) {
			int num = 3 + i * 2;
			out.print("\t\t\tcase " + num + ":\n");
			if( hasDivisor )
				out.print("\t\t\t\t" + opName + num + "(kernel,image,dest,divisor);\n");
			else
				out.print("\t\t\t\t" + opName + num + "(kernel,image,dest);\n");
			out.print("\t\t\t\tbreak;\n" +
					"\n");
		}
		out.print("\t\t\tdefault:\n" +
				"\t\t\t\treturn false;\n" +
				"\t\t}\n" +
				"\t\treturn true;\n" +
				"\t}\n\n");
	}

	public void addHorizontal(int num, boolean hasDivisor ) {
		String typeCast = generateTypeCast();

		out.print("\tpublic static void horizontal" + num + "( Kernel1D_" + typeKernel + " kernel , "
				+ typeInput + " image, " + typeOutput + " dest ");

		if( hasDivisor ) {
			out.print(", int divisor )\n");
		} else {
			out.print(")\n");
		}

		out.print("\t{\n" +
				"\t\tfinal " + dataInput + "[] dataSrc = image.data;\n" +
				"\t\tfinal " + dataOutput + "[] dataDst = dest.data;\n" +
				"\n");
		for (int i = 0; i < num; i++) {
			out.printf("\t\tfinal " + dataKernel + " k%d = kernel.data[%d];\n", (i + 1), i);
		}
		out.print("\n" +
				"\t\tfinal int radius = kernel.getRadius();\n" +
				"\n" +
				"\t\tfinal int width = image.getWidth();\n" +
				(hasDivisor ? declareHalf : "")+
				"\n" +
				"\t\tfor( int i = 0; i < image.height; i++ ) {\n" +
				"\t\t\tint indexDst = dest.startIndex + i*dest.stride+radius;\n" +
				"\t\t\tint j = image.startIndex + i*image.stride - radius;\n" +
				"\t\t\tfinal int jEnd = j+width-radius;\n" +
				"\n" +
				"\t\t\tfor( j += radius; j < jEnd; j++ ) {\n" +
				"\t\t\t\tint indexSrc = j;\n" +
				"\t\t\t\t" + sumType + " total = (dataSrc[indexSrc++]" + bitWise + ")*k1;\n");
		for (int i = 1; i < num - 1; i++) {
			out.printf("\t\t\t\ttotal += (dataSrc[indexSrc++]" + bitWise + ")*k%d;\n", (i + 1));
		}
		out.printf("\t\t\t\ttotal += (dataSrc[indexSrc]" + bitWise + ")*k%d;\n", num);
		out.printf("\n");
		if( hasDivisor ) {
			out.print("\t\t\t\tdataDst[indexDst++] = " + typeCast + "("+divide+");\n");
		} else {
			out.print("\t\t\t\tdataDst[indexDst++] = " + typeCast + "total;\n");
		}

		out.print("\t\t\t}\n" +
				"\t\t}\n" +
				"\t}\n\n");
	}

	public void addVertical(int num, boolean hasDivisor) {
		String typeCast = generateTypeCast();

		out.print("\tpublic static void vertical" + num + "( Kernel1D_" + typeKernel + " kernel , "
				 + typeInput + " image, " + typeOutput + " dest ");
		if( hasDivisor )
			out.print(", int divisor )\n");
		else
			out.print(")\n");

		out.print("\t{\n" +
				"\t\tfinal " + dataInput + "[] dataSrc = image.data;\n" +
				"\t\tfinal " + dataOutput + "[] dataDst = dest.data;\n" +
				"\n");
		for (int i = 0; i < num; i++) {
			out.printf("\t\tfinal " + dataKernel + " k%d = kernel.data[%d];\n", (i + 1), i);
		}
		out.print("\n" +
				"\t\tfinal int radius = kernel.getRadius();\n" +
				"\n" +
				"\t\tfinal int imgWidth = dest.getWidth();\n" +
				"\t\tfinal int imgHeight = dest.getHeight();\n" +
				(hasDivisor ? declareHalf : "") +
				"\n" +
				"\t\tfinal int yEnd = imgHeight-radius;\n" +
				"\n" +
				"\t\tfor( int y = radius; y < yEnd; y++ ) {\n" +
				"\t\t\tint indexDst = dest.startIndex+y*dest.stride;\n" +
				"\t\t\tint i = image.startIndex + (y-radius)*image.stride;\n" +
				"\t\t\tfinal int iEnd = i+imgWidth;\n" +
				"\n" +
				"\t\t\tfor( ; i < iEnd; i++ ) {\n" +
				"\t\t\t\tint indexSrc = i;\n" +
				"\n" +
				"\t\t\t\t" + sumType + " total = (dataSrc[indexSrc]"+bitWise+") * k1;\n");
		for (int i = 1; i < num; i++) {
			out.printf("\t\t\t\tindexSrc += image.stride;\n");
			out.printf("\t\t\t\ttotal += (dataSrc[indexSrc]" + bitWise + ")*k%d;\n", (i + 1));
		}
		out.print("\n");
		if( hasDivisor )
			out.print("\t\t\t\tdataDst[indexDst++] = " + typeCast + "("+divide+");\n");
		else
			out.print("\t\t\t\tdataDst[indexDst++] = " + typeCast + "total;\n");
		out.print("\t\t\t}\n" +
				"\t\t}\n" +
				"\t}\n\n");
	}

	public void addConvolve(int num ) {
		String typeCast = generateTypeCast();

		out.print("\tpublic static void convolve" + num + "( Kernel2D_" + typeKernel + " kernel, " + typeInput + " src, " + typeOutput + " dest)\n");

		out.print("\t{\n" +
				"\t\tfinal " + dataInput + "[] dataSrc = src.data;\n" +
				"\t\tfinal " + dataOutput + "[] dataDst = dest.data;\n" +
				"\n");
		out.print("\t\tfinal int width = src.getWidth();\n" +
				"\t\tfinal int height = src.getHeight();\n" +
				"\n" +
				"\t\tfinal int kernelRadius = kernel.getRadius();\n" +
				"\n" +
				"\t\tfor( int y = kernelRadius; y < height-kernelRadius; y++ ) {\n" +
				"\n" +
				"\t\t\t// first time through the value needs to be set\n");
		for( int i = 0; i < num; i++ ) {
			out.print("\t\t\t"+sumType+" k"+(i+1)+" = kernel.data["+i+"];\n");
		}
		out.print("\n" +
				"\t\t\tint indexDst = dest.startIndex + y*dest.stride+kernelRadius;\n" +
				"\t\t\tint indexSrcRow = src.startIndex+(y-kernelRadius)*src.stride-kernelRadius;\n" +
				"\t\t\tfor( int x = kernelRadius; x < width-kernelRadius; x++ ) {\n" +
				"\t\t\t\tint indexSrc = indexSrcRow + x;\n" +
				"\n" +
				"\t\t\t\t"+sumType+" total = 0;\n");
		for( int i = 0; i < num-1; i++ ) {
			out.print("\t\t\t\ttotal += (dataSrc[indexSrc++] "+bitWise+")* k"+(i+1)+";\n");
		}
		out.print("\t\t\t\ttotal += (dataSrc[indexSrc] "+bitWise+")* k"+num+";\n");
		out.print("\n" +
				"\t\t\t\tdataDst[indexDst++] = "+typeCast+"total;\n" +
				"\t\t\t}\n" +
				"\n" +
				"\t\t\t// rest of the convolution rows are an addition\n" +
				"\t\t\tfor( int i = 1; i < "+num+"; i++ ) {\n" +
				"\t\t\t\tindexDst = dest.startIndex + y*dest.stride+kernelRadius;\n" +
				"\t\t\t\tindexSrcRow = src.startIndex+(y+i-kernelRadius)*src.stride-kernelRadius;\n" +
				"\t\t\t\t\n");
		for( int i = 0; i < num; i++ ) {
			out.print("\t\t\t\tk"+(i+1)+" = kernel.data[i*"+num+" + "+i+"];\n");
		}
		out.print("\n" +
				"\t\t\t\tfor( int x = kernelRadius; x < width-kernelRadius; x++ ) {\n" +
				"\t\t\t\t\tint indexSrc = indexSrcRow+x;\n" +
				"\n" +
				"\t\t\t\t\t"+sumType+" total = 0;\n");
		for( int i = 0; i < num-1; i++ ) {
			out.print("\t\t\t\t\ttotal += (dataSrc[indexSrc++] "+bitWise+")* k"+(i+1)+";\n");
		}
		out.print("\t\t\t\t\ttotal += (dataSrc[indexSrc] "+bitWise+")* k"+(num)+";\n");
		out.print("\n" +
				"\t\t\t\t\tdataDst[indexDst++] += "+typeCast+"total;\n" +
				"\t\t\t\t}\n" +
				"\t\t\t}\n" +
				"\t\t}\n" +
				"\t}\n\n");
	}

	public void addConvolveDiv(int num ) {
		String typeCast = generateTypeCast();

		out.print("\tpublic static void convolve" + num + "( Kernel2D_" + typeKernel + " kernel, " + typeInput + " src, " + typeOutput + " dest , int divisor )\n");

		out.print("\t{\n" +
				"\t\tfinal " + dataInput + "[] dataSrc = src.data;\n" +
				"\t\tfinal " + dataOutput + "[] dataDst = dest.data;\n" +
				"\n");
		out.print("\t\tfinal int width = src.getWidth();\n" +
				"\t\tfinal int height = src.getHeight();\n" +
				declareHalf +
				"\n" +
				"\t\tfinal int kernelRadius = kernel.getRadius();\n");
		out.print("\t\tfinal "+sumType+" totalRow[] = new int[ width ];\n");
		out.print("\n" +
				"\t\tfor( int y = kernelRadius; y < height-kernelRadius; y++ ) {\n" +
				"\n" +
				"\t\t\t// first time through the value needs to be set\n");
		for( int i = 0; i < num; i++ ) {
			out.print("\t\t\t"+sumType+" k"+(i+1)+" = kernel.data["+i+"];\n");
		}
		out.print("\n" +
				"\t\t\tint indexSrcRow = src.startIndex+(y-kernelRadius)*src.stride-kernelRadius;\n" +
				"\t\t\tfor( int x = kernelRadius; x < width-kernelRadius; x++ ) {\n" +
				"\t\t\t\tint indexSrc = indexSrcRow + x;\n" +
				"\n" +
				"\t\t\t\t"+sumType+" total = 0;\n");
		for( int i = 0; i < num-1; i++ ) {
			out.print("\t\t\t\ttotal += (dataSrc[indexSrc++] "+bitWise+")* k"+(i+1)+";\n");
		}
		out.print("\t\t\t\ttotal += (dataSrc[indexSrc] "+bitWise+")* k"+num+";\n");
		out.print("\n" +
				"\t\t\t\ttotalRow[x] = total;\n" +
				"\t\t\t}\n" +
				"\n" +
				"\t\t\t// rest of the convolution rows are an addition\n" +
				"\t\t\tfor( int i = 1; i < "+num+"; i++ ) {\n" +
				"\t\t\t\tindexSrcRow = src.startIndex+(y+i-kernelRadius)*src.stride-kernelRadius;\n" +
				"\t\t\t\t\n");
		for( int i = 0; i < num; i++ ) {
			out.print("\t\t\t\tk"+(i+1)+" = kernel.data[i*"+num+" + "+i+"];\n");
		}
		out.print("\n" +
				"\t\t\t\tfor( int x = kernelRadius; x < width-kernelRadius; x++ ) {\n" +
				"\t\t\t\t\tint indexSrc = indexSrcRow+x;\n" +
				"\n" +
				"\t\t\t\t\t"+sumType+" total = 0;\n");
		for( int i = 0; i < num-1; i++ ) {
			out.print("\t\t\t\t\ttotal += (dataSrc[indexSrc++] "+bitWise+")* k"+(i+1)+";\n");
		}
		out.print("\t\t\t\t\ttotal += (dataSrc[indexSrc] "+bitWise+")* k"+(num)+";\n");
		out.print("\n" +
				"\t\t\t\t\ttotalRow[x] += total;\n" +
				"\t\t\t\t}\n" +
				"\t\t\t}\n" +
				"\t\t\tint indexDst = dest.startIndex + y*dest.stride+kernelRadius;\n" +
				"\t\t\tfor( int x = kernelRadius; x < width-kernelRadius; x++ ) {\n" +
				"\t\t\t\tdataDst[indexDst++] = "+typeCast+"((totalRow[x]+halfDivisor)/ divisor);\n" +
				"\t\t\t}\n"+
				"\t\t}\n" +
				"\t}\n\n");
	}

	private String generateTypeCast() {
		return sumType.compareTo(dataOutput) == 0 ? "" : "( " + dataOutput + " )";
	}

	public static void main(String args[]) throws FileNotFoundException {
		GenerateConvolvedUnrolled a = new GenerateConvolvedUnrolled();

		a.generate();
	}
}
